package org.springframework.social.lastfm.connect; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Properties; import org.springframework.core.io.ClassPathResource; import org.springframework.http.HttpHeaders; import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.http.converter.FormHttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.http.converter.json.MappingJacksonHttpMessageConverter; import org.springframework.social.lastfm.api.impl.LastFmApiMethodParameters; import org.springframework.social.lastfm.auth.LastFmAccessGrant; import org.springframework.social.lastfm.auth.LastFmAuthOperations; import org.springframework.social.lastfm.auth.LastFmAuthParameters; import org.springframework.social.support.ClientHttpRequestFactorySelector; import org.springframework.util.Assert; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.client.RestTemplate; /** * LastFmAuthOperations implementation that uses REST-template to make the OAuth * calls. * * @author Michael Lavelle */ public class LastFmAuthTemplate implements LastFmAuthOperations { private final String clientId; private final String clientSecret; private final String accessTokenUrl; private final String authorizeUrl; private final RestTemplate restTemplate; public LastFmAuthTemplate(String clientId, String clientSecret) { this(clientId, clientSecret, "http://www.last.fm/api/auth/", "http://ws.audioscrobbler.com/2.0/"); } public LastFmAuthTemplate(String clientId, String clientSecret, String authorizeUrl, String accessTokenUrl) { Assert.notNull(clientId, "The clientId property cannot be null"); Assert.notNull(clientSecret, "The clientSecret property cannot be null"); Assert.notNull(authorizeUrl, "The authorizeUrl property cannot be null"); Assert.notNull(accessTokenUrl, "The accessTokenUrl property cannot be null"); this.clientId = clientId; this.clientSecret = clientSecret; String clientInfo = "?api_key=" + formEncode(clientId); this.authorizeUrl = authorizeUrl + clientInfo; this.accessTokenUrl = accessTokenUrl; this.restTemplate = createRestTemplate(true); } /** * Set the request factory on the underlying RestTemplate. This can be used * to plug in a different HttpClient to do things like configure custom SSL * settings. */ public void setRequestFactory(ClientHttpRequestFactory requestFactory) { Assert.notNull(requestFactory, "The requestFactory property cannot be null"); this.restTemplate.setRequestFactory(requestFactory); } public String buildAuthorizeUrl(LastFmAuthParameters parameters) { return buildAuthUrl(authorizeUrl, parameters); } public LastFmAccessGrant exchangeForAccess(String token, MultiValueMap<String, String> additionalParameters) { MultiValueMap<String, String> params = new LinkedMultiValueMap<String, String>(); params.set("api_key", clientId); params.set("secret", clientSecret); params.set("token", token); if (additionalParameters != null) { params.putAll(additionalParameters); } return postForAccessGrant(accessTokenUrl, params); } // subclassing hooks private String getUserAgent() { String pomVersion = getPomVersion(); return "spring-social-lastfm" + (pomVersion == null ? "" : "/" + pomVersion); } private String getPomVersion() { Properties properties = new Properties(); try { properties.load(new ClassPathResource("project.properties").getInputStream()); return properties.getProperty("pom.version"); } catch (IOException e) { return null; } } /** * Creates the {@link RestTemplate} used to communicate with the provider's * OAuth 2 API. This implementation creates a RestTemplate with a minimal * set of HTTP message converters ({@link FormHttpMessageConverter} and * {@link MappingJacksonHttpMessageConverter}). May be overridden to * customize how the RestTemplate is created. For example, if the provider * returns data in some format other than JSON for form-encoded, you might * override to register an appropriate message converter. */ protected RestTemplate createRestTemplate(boolean json) { HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.add("User-Agent", getUserAgent()); RestTemplate restTemplate = new RestTemplateWithHeaders( ClientHttpRequestFactorySelector.getRequestFactory(), httpHeaders); List<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>( 2); converters.add(new FormHttpMessageConverter()); if (json) { converters.add(new MappingJackson2HttpMessageConverter()); } restTemplate.setMessageConverters(converters); return restTemplate; } /** * Posts the request for an access grant to the provider. The default * implementation uses RestTemplate to request the access token and expects * a JSON response to be bound to a Map. The information in the Map will be * used to create an {@link LastFmAccessGrant}. Since the OAuth 2 * specification indicates that an access token response should be in JSON * format, there's often no need to override this method. If all you need to * do is capture provider-specific data in the response, you should override * createAccessGrant() instead. However, in the event of a provider whose * access token response is non-JSON, you may need to override this method * to request that the response be bound to something other than a Map. For * example, if the access token response is given as form-encoded, this * method should be overridden to call RestTemplate.postForObject() asking * for the response to be bound to a MultiValueMap (whose contents can then * be used to create an AccessGrant). * * @param accessTokenUrl * the URL of the provider's access token endpoint. * @param parameters * the parameters to post to the access token endpoint. * @return the access grant. */ @SuppressWarnings("unchecked") protected LastFmAccessGrant postForAccessGrant(String accessTokenUrl, MultiValueMap<String, String> parameters) { String apiKey = parameters.getFirst("api_key"); String token = parameters.getFirst("token"); String secret = parameters.getFirst("secret"); MultiValueMap<String, String> authParams = new LastFmApiMethodParameters( "auth.getSession", apiKey, token, secret, new HashMap<String, String>()); return extractAccessGrant(token, restTemplate.postForObject( accessTokenUrl, authParams, Map.class)); } /** * Creates an {@link LastFmAccessGrant} given the response from the access * token exchange with the provider. May be overridden to create a custom * AccessGrant that captures provider-specific information from the access * token response. * * @param accessToken * the access token value received from the provider * @param scope * the scope of the access token * @param refreshToken * a refresh token value received from the provider * @param expiresIn * the time (in seconds) remaining before the access token * expires. * @param response * all parameters from the response received in the access token * exchange. * @return an {@link LastFmAccessGrant} */ protected LastFmAccessGrant createAccessGrant(String token, String sessionKey, Map<String, Object> result) { return new LastFmAccessGrant(token, sessionKey); } // testing hooks protected RestTemplate getRestTemplate() { return restTemplate; } // internal helpers private String buildAuthUrl(String baseAuthUrl, LastFmAuthParameters parameters) { StringBuilder authUrl = new StringBuilder(baseAuthUrl); for (Iterator<Entry<String, List<String>>> additionalParams = parameters .entrySet().iterator(); additionalParams.hasNext();) { Entry<String, List<String>> param = additionalParams.next(); String name = formEncode(param.getKey()); for (Iterator<String> values = param.getValue().iterator(); values .hasNext();) { authUrl.append('&').append(name).append('=') .append(formEncode(values.next())); } } return authUrl.toString(); } private String formEncode(String data) { try { return URLEncoder.encode(data, "UTF-8"); } catch (UnsupportedEncodingException ex) { // should not happen, UTF-8 is always supported throw new IllegalStateException(ex); } } @SuppressWarnings("unchecked") private LastFmAccessGrant extractAccessGrant(String token, Map<String, Object> result) { return createAccessGrant(token, (String) ((Map<String, Object>) result.get("session")) .get("key"), result); } }